// Copyright (C) 2010-2020 Joel Rosdahl and other contributors
//
// See doc/AUTHORS.adoc for a complete list of contributors.
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation; either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program; if not, write to the Free Software Foundation, Inc., 51
// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

#include "lockfile.hpp"

#include "Util.hpp"
#include "logging.hpp"

// This function acquires a lockfile for the given path. Returns true if the
// lock was acquired, otherwise false. If the lock has been considered stale
// for the number of microseconds specified by staleness_limit, the function
// will (if possible) break the lock and then try to acquire it again. The
// staleness limit should be reasonably larger than the longest time the lock
// can be expected to be held, and the updates of the locked path should
// probably be made with an atomic rename(2) to avoid corruption in the rare
// case that the lock is broken by another process.
bool
lockfile_acquire(const char* path, unsigned staleness_limit)
{
  char* lockfile = format("%s.lock", path);
  char* my_content = nullptr;
  char* content = nullptr;
  char* initial_content = nullptr;
  const char* hostname = get_hostname();
  bool acquired = false;
  unsigned to_sleep = 1000; // Microseconds.
  unsigned slept = 0;       // Microseconds.

  while (true) {
    free(my_content);
    my_content =
      format("%s:%d:%d", hostname, (int)getpid(), (int)time(nullptr));

#if defined(_WIN32) || defined(__CYGWIN__)
    int fd = open(lockfile, O_WRONLY | O_CREAT | O_EXCL | O_BINARY, 0666);
    if (fd == -1) {
      int saved_errno = errno;
      cc_log("lockfile_acquire: open WRONLY %s: %s", lockfile, strerror(errno));
      if (saved_errno == ENOENT) {
        // Directory doesn't exist?
        if (Util::create_dir(Util::dir_name(lockfile))) {
          // OK. Retry.
          continue;
        }
      }
      if (saved_errno != EEXIST) {
        // Directory doesn't exist or isn't writable?
        goto out;
      }
      // Someone else has the lock.
      fd = open(lockfile, O_RDONLY | O_BINARY);
      if (fd == -1) {
        if (errno == ENOENT) {
          // The file was removed after the open() call above, so retry
          // acquiring it.
          continue;
        } else {
          cc_log(
            "lockfile_acquire: open RDONLY %s: %s", lockfile, strerror(errno));
          goto out;
        }
      }
      free(content);
      const size_t bufsize = 1024;
      content = static_cast<char*>(x_malloc(bufsize));
      int len = read(fd, content, bufsize - 1);
      if (len == -1) {
        cc_log("lockfile_acquire: read %s: %s", lockfile, strerror(errno));
        close(fd);
        goto out;
      }
      close(fd);
      content[len] = '\0';
    } else {
      // We got the lock.
      if (write(fd, my_content, strlen(my_content)) == -1) {
        cc_log("lockfile_acquire: write %s: %s", lockfile, strerror(errno));
        close(fd);
        x_unlink(lockfile);
        goto out;
      }
      close(fd);
      acquired = true;
      goto out;
    }
#else
    if (symlink(my_content, lockfile) == 0) {
      // We got the lock.
      acquired = true;
      goto out;
    }
    int saved_errno = errno;
    cc_log("lockfile_acquire: symlink %s: %s", lockfile, strerror(saved_errno));
    if (saved_errno == ENOENT) {
      // Directory doesn't exist?
      if (Util::create_dir(Util::dir_name(lockfile))) {
        // OK. Retry.
        continue;
      }
    }
    if (saved_errno == EPERM) {
      // The file system does not support symbolic links. We have no choice but
      // to grant the lock anyway.
      acquired = true;
      goto out;
    }
    if (saved_errno != EEXIST) {
      // Directory doesn't exist or isn't writable?
      goto out;
    }
    free(content);
    content = x_strdup(Util::read_link(lockfile).c_str());
    if (str_eq(content, "")) {
      if (errno == ENOENT) {
        // The symlink was removed after the symlink() call above, so retry
        // acquiring it.
        continue;
      } else {
        cc_log("lockfile_acquire: readlink %s: %s", lockfile, strerror(errno));
        goto out;
      }
    }
#endif

    if (str_eq(content, my_content)) {
      // Lost NFS reply?
      cc_log("lockfile_acquire: symlink %s failed but we got the lock anyway",
             lockfile);
      acquired = true;
      goto out;
    }
    // A possible improvement here would be to check if the process holding the
    // lock is still alive and break the lock early if it isn't.
    cc_log("lockfile_acquire: lock info for %s: %s", lockfile, content);
    if (!initial_content) {
      initial_content = x_strdup(content);
    }
    if (slept > staleness_limit) {
      if (str_eq(content, initial_content)) {
        // The lock seems to be stale -- break it.
        cc_log("lockfile_acquire: breaking %s", lockfile);
        // Try to acquire path.lock.lock:
        if (lockfile_acquire(lockfile, staleness_limit)) {
          lockfile_release(path);     // Remove path.lock
          lockfile_release(lockfile); // Remove path.lock.lock
          to_sleep = 1000;
          slept = 0;
          continue;
        }
      }
      cc_log("lockfile_acquire: gave up acquiring %s", lockfile);
      goto out;
    }
    cc_log("lockfile_acquire: failed to acquire %s; sleeping %u microseconds",
           lockfile,
           to_sleep);
    usleep(to_sleep);
    slept += to_sleep;
    to_sleep *= 2;
  }

out:
  if (acquired) {
    cc_log("Acquired lock %s", lockfile);
  } else {
    cc_log("Failed to acquire lock %s", lockfile);
  }
  free(lockfile);
  free(my_content);
  free(initial_content);
  free(content);
  return acquired;
}

// Release the lockfile for the given path. Assumes that we are the legitimate
// owner.
void
lockfile_release(const char* path)
{
  char* lockfile = format("%s.lock", path);
  cc_log("Releasing lock %s", lockfile);
  tmp_unlink(lockfile);
  free(lockfile);
}

#ifdef TEST_LOCKFILE
int
main(int argc, char** argv)
{
  extern char* cache_logfile;
  cache_logfile = "/dev/stdout";
  if (argc == 4) {
    unsigned staleness_limit = atoi(argv[1]);
    if (str_eq(argv[2], "acquire")) {
      return lockfile_acquire(argv[3], staleness_limit) == 0;
    } else if (str_eq(argv[2], "release")) {
      lockfile_release(argv[3]);
      return 0;
    }
  }
  fprintf(stderr,
          "Usage: testlockfile <staleness_limit> <acquire|release> <path>\n");
  return 1;
}
#endif
